// ============================================================================
// FileName: SIPRegistrationAgent.cs
//
// Description:
// Registration agent daemon to maintain SIP registrations with multiple SIP
// Registrar servers.
//
// Author(s):
// Aaron Clauson
//
// History:
// 13 Nov 2006	Aaron Clauson	Created.
// 19 Oct 2007  Aaron Clauson   Incorporated DNS management to stop sipswitch stalling on invalid host names.
// 17 May 2008  Aaron Clauson   Refactored UserRegistration class into its own class file.
//
// License: 
// This software is licensed under the BSD License http://www.opensource.org/licenses/bsd-license.php
//
// Copyright (c) Aaron Clauson (aaronc@blueface.ie), Blue Face Ltd, Dublin, Ireland (www.blueface.ie)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that 
// the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 
// disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Blue Face Ltd. 
// nor the names of its contributors may be used to endorse or promote products derived from this software without specific 
// prior written permission. 
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
// BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
// POSSIBILITY OF SUCH DAMAGE.
// ============================================================================

using System;
using System.Collections.Generic;
using System.Data;
using System.Net;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using BlueFace.VoIP.Authentication;
using BlueFace.VoIP.Net;
using BlueFace.VoIP.Net.SIP;
using BlueFace.VoIP.SIPServerCores.StatefulProxy;
using BlueFace.Sys;
using BlueFace.Sys.Net;
using Heijden.DNS;
using log4net;

#if UNITTEST
using NUnit.Framework;
#endif

namespace BlueFace.VoIP.SIPServer
{
    public delegate void RegistrationStateChangedDelegate(UserRegistration userRegistration);

    public enum SIPContactRegistrationEnum
    {
        RegisterBinding = 1,
        Removebinding = 2,
        CheckContacts = 3,
    }
    
    public class SIPRegistrationAgent
	{
        public const string SIP_USERAGENT_STRING = "www.mysipswitch.com";
        public const int INITIAL_REGISTRATION_BUCKETS = 60;
        public const int REGISTRATION_RENEWAL_PERIOD = 1000;        // Time in milliseconds between the registration agent checking registrations.
        public const int REGISTRATION_HEAD_TIME = 5;                // Time in seconds to go to next registration to initate.
        public const int REGISTER_FAILURERETRY_INTERVAL = 300;      // Number of seconds between consecutive register requests in the event of failures or timeouts.
        public const int REGISTER_FAILEDDNS_RETRY_INTERVAL = 180;   // Minimum number of seconds between consecutive register requests in the event of a DNS failure resolving the registrar server.
        public const int REGISTER_EMPTYDNS_RETRY_INTERVAL = 2;      // When the DNS manager has not yet had time to do the lookup wait this number of seconds and try again.
        public const int REGISTER_CHECKTIME_THRESHOLD = 3;          // Time the user registration checks should be taking less than. If exceeded a log message is produced.
        public const int REGISTER_EXPIREALL_WAITTIME = 2000;        // When stopping the registration agent the time to give after the initial request for all requests to complete.
        public const int REGISTER_DELETION_TIMEOUT = 60;            // Number of seconds a deletion request will timeout after.
        private const char CUSTOM_HEADERS_SEPARATOR = '|';

        private static ILog logger = AppState.GetLogger("sipregagent");
        
        private bool m_sendRegisters = true;    // While true the register agent thread will send out register requests to maintain it's registrations.

        private SIPTransport m_sipTransport;
        private IPEndPoint m_proxyEndPoint;

        private Dictionary<Guid, UserRegistration> m_registrationsById = new Dictionary<Guid, UserRegistration>();           // Provides a lookup mechanism to enable querying and deletion of specifc registration instances.

        public event ProxyLogDelegate StatefulProxyLogEvent;
        public event RegistrationStateChangedDelegate RegistrationStateChanged;      // Fired when the state of a user registration changes on a completed registration attempt.

        public Int64 Count
        {
            get { return m_registrationsById.Count; }
        }

        public SIPRegistrationAgent(SIPTransport sipTransport, IPEndPoint proxyEndPoint, List<UserRegistration> userResgistrations)
		{
            m_sipTransport = sipTransport;
            m_proxyEndPoint = proxyEndPoint;

            if (userResgistrations != null && userResgistrations.Count > 0)
            {
                int bucket = 0;

                foreach (UserRegistration userReg in userResgistrations)
                {
                    userReg.BucketId = bucket;
                    //m_userRegistrations.Add(userReg);
                    m_registrationsById.Add(userReg.RegistrationId, userReg);
                    bucket = (bucket + 1) % INITIAL_REGISTRATION_BUCKETS;
                }
            }
        }

        /// <summary>
        /// If the user registrations list has been changed this methid can be used to check the existing list against the new list and add and
        /// remove registrations as required.
        /// </summary>
        /// <param name="userResgistrations"></param>
        public void Restart(List<UserRegistration> userResgistrations)
        {
            try
            {
                Stop();

                m_registrationsById.Clear();

                if (userResgistrations != null && userResgistrations.Count > 0)
                {
                    int bucket = 0;

                    foreach (UserRegistration userReg in userResgistrations)
                    {
                        userReg.BucketId = bucket;
                        //m_userRegistrations.Add(userReg);
                        m_registrationsById.Add(userReg.RegistrationId, userReg);
                        bucket = (bucket + 1) % INITIAL_REGISTRATION_BUCKETS;
                    }
                }

                m_sendRegisters = true;
                Start();
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPRegistrationAgent Restart. " + excp.Message);
            }
        }

		public void Start()
		{
            logger.Debug("SIPRegistrationAgent thread started.");

            Thread regThread = new Thread(new ThreadStart(MonitorRegistrations));
	        regThread.Start();	
		}

        public void AddRegistration(UserRegistration userRegistration)
        {
            logger.Debug("AddRegistration for " + userRegistration.Username + ", server " + userRegistration.Registrar + ".");

            if (m_registrationsById.ContainsKey(userRegistration.RegistrationId))
            {
                // This is a registration update.
                UserRegistration existingRegistration = m_registrationsById[userRegistration.RegistrationId];
                existingRegistration.AuthUsername = userRegistration.AuthUsername;
                existingRegistration.ExpirySeconds = userRegistration.ExpirySeconds;
                existingRegistration.OriginalExpirySeconds = userRegistration.ExpirySeconds;
                existingRegistration.Password = userRegistration.Password;
                existingRegistration.Username = userRegistration.Username;
                existingRegistration.Realm = userRegistration.Realm;
                existingRegistration.Registrar = userRegistration.Registrar;
                existingRegistration.CustomHeaders = userRegistration.CustomHeaders;

                SIPURI originalURI = existingRegistration.Contact.CopyOf();
                originalURI.Parameters.Remove(UserRegistration.CONTACT_ID_KEY);

                if (originalURI.CanonicalAddress != userRegistration.Contact.CanonicalAddress)
                {
                    SendContactRemovalRequest(existingRegistration.RegistrationId, existingRegistration.Contact);
                    existingRegistration.Contact = userRegistration.Contact;
                }

                existingRegistration.NextRegistrationTime = DateTime.Now;
            }
            else
            {
                // This is a new registration add to the lists.
                //lock (m_userRegistrations)
                //{
                //    m_userRegistrations.Add(userRegistration);
                //}

                lock (m_registrationsById)
                {
                    m_registrationsById.Add(userRegistration.RegistrationId, userRegistration);
                }
            }
        }

        public void DeleteRegistration(Guid registrationId)
        {
            try
            {
                logger.Debug("DeleteRegistration for " + registrationId + ".");

                if (m_registrationsById.ContainsKey(registrationId))
                {
                    UserRegistration delReg = m_registrationsById[registrationId];

                    // Don't remove the registration form the list here. Let the monitor loop do it so that any current bindings will be expired.
                    delReg.DeleteRequired = true;
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception DeleteRegistration. " + excp.Message);
            }
        }

        public List<UserRegistration> GetOwnerRegistrations(string owner)
        {
            try
            {
                List<UserRegistration> ownerRegs = new List<UserRegistration>();

                foreach (UserRegistration currReg in m_registrationsById.Values)
                {
                    if (currReg.Owner == owner)
                    {
                        ownerRegs.Add(currReg);
                    }
                }

                return ownerRegs;
            }
            catch (Exception excp)
            {
                logger.Error("Exception GetOwnerRegistrations. " + excp.Message);
                throw excp;
            }
        }

        public UserRegistration Get(Guid registrationId)
        {
            if (m_registrationsById.ContainsKey(registrationId))
            {
                return m_registrationsById[registrationId];
            }

            return null;
        }

		/// <summary>
		/// Retrieve a list of accounts that the agent will register for from the database and then monitor them and any additional ones inserte.
		/// </summary>
        public void MonitorRegistrations()
        {
            try
            {
                bool useBuckets = m_registrationsById.Count > INITIAL_REGISTRATION_BUCKETS;
                int currentBucket = 0;

                while (m_sendRegisters)
                {
                    try
                    {
                        List<UserRegistration> deleteReqdReg = new List<UserRegistration>();    // Will record any registrations that have been marked for deletion.

                        //FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.Timing, "Commencing user registrations check.", null));
                        DateTime startCheckTime = DateTime.Now;
                        int registrationsSent = 0;
                        int registrationsChecked = 0;
                        //Console.WriteLine("Commencing user registrations check.");

                        lock (m_registrationsById)
                        {
                            foreach (UserRegistration userReg in m_registrationsById.Values)
                            {
                                try
                                {
                                    //Console.WriteLine("Checking user registration " + userReg.Username + " at " + userReg.Registrar + ".");

                                    // Check to see if this registration is marked for deletion.
                                    if (userReg.DeleteRequired && userReg.DeleteSent == DateTime.MinValue)
                                    {
                                        deleteReqdReg.Add(userReg);
                                        continue;
                                    }

                                    // Use buckets when starting up but after that send registrations as they become due.
                                    if ((userReg.BucketId == currentBucket || !useBuckets) && DateTime.Now > userReg.NextRegistrationTime)
                                    {
                                        FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterInProgress, "Checking user registration " + userReg.Username + " at " + userReg.Registrar + ".", userReg.Owner));
                                        registrationsChecked++;
                                        string registrarServer = IPSocket.ParseHostFromSocket(userReg.Registrar);
                                        DNSResponse dnsEntry = DNSManager.LookupAsync(registrarServer);

                                        if (dnsEntry == null)
                                        {
                                            //logger.Debug("DNS Manager does not currently have an entry for " + IPSocket.ParseHostFromSocket(userReg.Registrar) + ", wait " + REGISTER_EMPTYDNS_RETRY_INTERVAL + "s and try again.");
                                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterInProgress, "DNS Manager does not currently have an entry for " + userReg.Registrar + ", delaying " + REGISTER_EMPTYDNS_RETRY_INTERVAL + "s.", userReg.Owner));
                                            userReg.NextRegistrationTime = DateTime.Now.AddSeconds(REGISTER_EMPTYDNS_RETRY_INTERVAL);
                                        }
                                        else if (dnsEntry.Error != null || dnsEntry.RecordsA == null || dnsEntry.RecordsA.Length == 0)
                                        {
                                            //logger.Warn("Error resolving " + IPSocket.ParseHostFromSocket(userReg.Registrar) + ". " + dnsEntry.Error);
                                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.DNS, "Error resolving " + IPSocket.ParseHostFromSocket(userReg.Registrar) + " please check. " + dnsEntry.Error, userReg.Owner));

                                            // For SIP Registrars that can't be found in DNS only retry at a longer interval.
                                            userReg.FailureMessage = "Could not resolve " + userReg.Registrar + ". " + dnsEntry.Error;
                                            userReg.NextRegistrationTime = DateTime.Now.AddSeconds(REGISTER_FAILEDDNS_RETRY_INTERVAL);

                                            if (RegistrationStateChanged != null)
                                            {
                                                RegistrationStateChanged(userReg);
                                            }
                                        }
                                        else
                                        {
                                            userReg.LastRegisterAttempt = DateTime.Now;
                                            userReg.NextRegistrationTime = DateTime.Now.AddSeconds(REGISTER_FAILURERETRY_INTERVAL);
                                            userReg.RegistrationAgentIPEndPoint = m_sipTransport.GetTransportContact(userReg.RegistrarIPEndPoint);
                                            userReg.RegisterInProgress = true;
                                            userReg.ContactRegisterType = SIPContactRegistrationEnum.RegisterBinding;
                                            SendInitialRegister(userReg, userReg.RegistrationAgentIPEndPoint, dnsEntry.RecordsA[0].Address, userReg.OriginalExpirySeconds);

                                            registrationsSent++;
                                        }
                                    }
                                }
                                catch (Exception regExcp)
                                {
                                    logger.Error("Exception MonitorRegistrations attempting register for " + userReg.Username + " and " + userReg.Registrar + ". " + regExcp.Message);

                                    try
                                    {
                                        // Regsitrations that generate exceptions are disabled pending user intervention.
                                        userReg.Disabled = true;
                                        userReg.RegisterInProgress = false;
                                        userReg.DeleteRequired = true;
                                        userReg.FailureMessage = "Exception. " + regExcp.Message;

                                        if (RegistrationStateChanged != null)
                                        {
                                            RegistrationStateChanged(userReg);
                                        }
                                    }
                                    catch (Exception disableExcp)
                                    {
                                        logger.Error("Exception MonitorRegistrations (disabling registration). " + disableExcp);
                                    }
                                }
                            }
                        }

                        #region Remove any registrations in the remove list.

                        foreach (UserRegistration delReg in deleteReqdReg)
                        {
                            if (delReg.Registered && DateTime.Now.Subtract(delReg.DeleteSent).TotalSeconds > REGISTER_DELETION_TIMEOUT)
                            {
                                RegistrationComplete(delReg.RegistrationId);
                            }
                            else if (delReg.Registered)
                            {
                                FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRemoval, "Registration deletion requested, removing contact " + delReg.Contact.ToString() + ".", delReg.Owner));
                                SendContactRemovalRequest(delReg.RegistrationId, delReg.Contact);
                            }
                            else
                            {
                                RegistrationComplete(delReg.RegistrationId);
                            }
                        }

                        #endregion

                        //Console.WriteLine("Registration agent check complete.");

                        //FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.Timing, "Completed user registrations check.", null));
                        double userRegCheckTime = DateTime.Now.Subtract(startCheckTime).TotalSeconds;
                        //if (userRegCheckTime > REGISTER_CHECKTIME_THRESHOLD)
                        //{
                        //logger.Debug("User registrations check time took " + userRegCheckTime.ToString("0.##") + "s.");

                        //if (currentBucket != -1)
                        //{
                        //    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.Timing, "User registrations check time for bucket " + currentBucket + " was " + userRegCheckTime.ToString("0.##") + "s, checked=" + registrationsChecked + ", sent=" + registrationsSent + ", total=" + m_registrationsById.Count + ".", null));
                        //}
                        //else
                        //{
                        //    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.Timing, "User registrations check time was " + userRegCheckTime.ToString("0.##") + "s, checked=" + registrationsChecked + ", sent=" + registrationsSent + ", total=" + m_registrationsById.Count + " (in progress transactions invites=" + SIPTransaction.CountInvites + ", non-invites=" + SIPTransaction.CountNonInvites + ", reliable transmissions=" + SIPTransport.ReliableTrasmissionsCount + ").", null));
                        //}

                        //}

                        if (useBuckets)
                        {
                            currentBucket++;
                            if (currentBucket == INITIAL_REGISTRATION_BUCKETS)
                            {
                                useBuckets = false;
                            }
                        }
                        else
                        {
                            currentBucket = -1;
                        }

                        Thread.Sleep(REGISTRATION_RENEWAL_PERIOD);
                    }
                    catch (Exception monExcp)
                    {
                        FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.Error, "Exception in Monitor Loop. " + monExcp.Message, null));
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception MonitorRegistrations. " + excp.Message);
            }
        }

        private void SendInitialRegister(UserRegistration userReg, IPEndPoint localEndPoint, IPAddress registrarIPAddress, int expirySeconds)
        {
            try
            {
                int port = (IPSocket.ParsePortFromSocket(userReg.Registrar) != 0) ? IPSocket.ParsePortFromSocket(userReg.Registrar) : SIPConstants.DEFAULT_SIP_PORT;
                userReg.RegistrarIPEndPoint = new IPEndPoint(registrarIPAddress, port);
                userReg.CSeq = userReg.CSeq + 1;

                FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterInProgress, "Initiating registration for " + userReg.Username + " on " + userReg.Registrar + ".", userReg.Owner));
                SIPRequest regRequest = GetRegistrationRequest(userReg, localEndPoint, expirySeconds, userReg.RegistrarIPEndPoint);

                //SIPNonInviteTransaction regTransaction = m_sipTransport.CreateNonInviteTransaction(regRequest, userReg.RegistrarIPEndPoint, m_sipTransport.GetTransportContact(null), SIPProtocolsEnum.UDP);
                SIPNonInviteTransaction regTransaction = m_sipTransport.CreateNonInviteTransaction(regRequest, m_proxyEndPoint, m_sipTransport.GetTransportContact(null), SIPProtocolsEnum.UDP);
                regTransaction.NonInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(ServerResponseReceived);
                regTransaction.NonInviteTransactionTimedOut += new SIPTransactionTimedOutDelegate(regTransaction_TransactionTimedOut);

                m_sipTransport.SendSIPReliable(regTransaction);
            }
            catch (Exception excp)
            {
                logger.Error("Exception SendInitialRegister for " + userReg.Owner + " and " + userReg.Registrar + ". " + excp.Message);
            }
        }

        private void regTransaction_TransactionTimedOut(SIPTransaction sipTransaction)
        {
            try
            {
                SIPRequest sipRequest = sipTransaction.TransactionRequest;
                Guid callIdGuid = new Guid(sipRequest.Header.CallId);

                if (m_registrationsById.ContainsKey(callIdGuid))
                {
                    UserRegistration userReg = m_registrationsById[callIdGuid];

                    //logger.Warn("Registration timed out for for " + userReg.Username + " and " + userReg.Registrar + " owner " + userReg.Owner + ", next attempt in " + REGISTER_RETRY_INTERVAL + "s.");
                    //userReg.NextRegistrationTime = DateTime.Now.AddSeconds(REGISTER_FAILURERETRY_INTERVAL);
                    userReg.FailureMessage = "Registration to " + userReg.Registrar + " timed out.";
                    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration timed out for " + userReg.Username + " on " + userReg.Registrar + ", next attempt in " + REGISTER_FAILURERETRY_INTERVAL + "s.", userReg.Owner));

                    if (RegistrationStateChanged != null)
                    {
                        RegistrationStateChanged(userReg);
                    }
                }
                else
                {
                    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.Warn, "Registration request timed for unmatched call originally to " + sipTransaction.RemoteEndPoint + ".", null));
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception regTransaction_TransactionTimedOut. " + excp.Message);
            }
        }

        /// <summary>
        /// The event handler for responses to the initial register request.
        /// </summary>
        public void ServerResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse)
        {
            try
            {
                Guid callIdGuid = new Guid(sipResponse.Header.CallId);

                if (m_registrationsById.ContainsKey(callIdGuid))
                {
                    UserRegistration userReg = m_registrationsById[callIdGuid];

                    if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised)
                    {
                        if (sipResponse.Header.AuthenticationHeader != null)
                        {
                            #region Responding to server authentication request.

                            string nonce = sipResponse.Header.AuthenticationHeader.AuthRequest.Nonce;
                            string realm = sipResponse.Header.AuthenticationHeader.AuthRequest.Realm;
                            string username = (userReg.AuthUsername != null && userReg.AuthUsername.Trim().Length > 0) ? userReg.AuthUsername : userReg.Username;
                            AuthorizationRequest authRequest = null; 

                            userReg.CSeq = userReg.CSeq + 1;

                            SIPRequest regRequest = sipTransaction.TransactionRequest.Copy();
                            regRequest.Header.Via.TopViaHeader.Branch = CallProperties.CreateBranchId();
                            regRequest.Header.From.FromTag = CallProperties.CreateNewTag();
                            regRequest.Header.To.ToTag = null;
                            regRequest.Header.CSeq = userReg.CSeq;

                            // This is for sip.aol.com, the only provider encountered that requires the extra authentication headers.
                            if (sipResponse.Header.WWWAuthenticate && sipResponse.Header.AuthenticationHeader.AuthRequest.Qop == "auth")
                            {
                                regRequest.Header.Authorize = true;
                                string paddingStr = "000000000";
                                string nonceCountStr = paddingStr.Substring(0, 9 - userReg.NonceCount.ToString().Length) + userReg.NonceCount;
                                userReg.NonceCount++;
                                authRequest = new AuthorizationRequest(realm, username, userReg.Password, sipTransaction.TransactionRequest.URI.ToString(), nonce, SIPMethodsEnum.REGISTER.ToString(), Crypto.GetRandomInt().ToString(), nonceCountStr, "auth");
                            }
                            else
                            {
                                authRequest = new AuthorizationRequest(realm, username, userReg.Password, sipTransaction.TransactionRequest.URI.ToString(), nonce, SIPMethodsEnum.REGISTER.ToString());

                                if (sipResponse.Header.WWWAuthenticate)
                                {
                                     regRequest.Header.Authorize = true;
                                }
                                else if (sipResponse.Header.ProxyAuthenticate)
                                {
                                    regRequest.Header.ProxyAuthorization = true;
                                }
                                else if (sipResponse.Header.Authorize)
                                {
                                    regRequest.Header.Authorize = true;
                                }
                                else
                                {
                                    regRequest.Header.ProxyAuthorization = true;
                                }
                            }

                            regRequest.Header.AuthenticationHeader = new SIPAuthenticationHeader(authRequest);
                            regRequest.Header.AuthenticationHeader.AuthRequest.Response = authRequest.Digest;

                            //logger.Debug("Auth header: " + regRequest.Header.AuthenticationHeader.ToString());

                            // If this request has been marked for deletion send a 0 expiry time.
                            if (userReg.DeleteRequired)
                            {
                                regRequest.Header.Expires = 0;
                            }

                            //SIPNonInviteTransaction regAuthTransaction = m_sipTransport.CreateNonInviteTransaction(regRequest, userReg.RegistrarIPEndPoint, localEndPoint, userReg.Protocol);
                            SIPNonInviteTransaction regAuthTransaction = m_sipTransport.CreateNonInviteTransaction(regRequest, m_proxyEndPoint, localEndPoint, userReg.Protocol);
                            regAuthTransaction.NonInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(AuthResponseReceived);
                            regAuthTransaction.NonInviteTransactionTimedOut += new SIPTransactionTimedOutDelegate(regTransaction_TransactionTimedOut);

                            m_sipTransport.SendSIPReliable(regAuthTransaction);

                            #endregion
                        }
                        else
                        {
                            userReg.Registered = false;
                            userReg.FailureMessage = "Server did not provide auth header, check realm.";
                            userReg.NextRegistrationTime = DateTime.Now.AddSeconds(REGISTER_FAILURERETRY_INTERVAL);
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration failed for " + userReg.Username + " on " + userReg.Registrar + ", the server did not respond with an authentication header, check realm.", userReg.Owner));

                            RegistrationComplete(callIdGuid);
                        }
                    }
                    else
                    {
                        // Non 401 or 407 responses mean the registration attempt is finished and the call-id will be removed and state updated.
                        if (sipResponse.Status == SIPResponseStatusCodesEnum.Ok)
                        {
                            // Successful registration.
                            OkResponseReceived(sipTransaction, remoteEndPoint, sipResponse);
                        }
                        else if (sipResponse.Status == SIPResponseStatusCodesEnum.Forbidden || sipResponse.Status == SIPResponseStatusCodesEnum.NotFound)
                        {
                            // SIP account does not appear to exist. Disable registration attempts until user intervenes to correct.
                            userReg.Registered = false;
                            userReg.FailureMessage = "Authentication failed (" + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + ").";
                            //userReg.NextRegistrationTime = DateTime.Now.AddSeconds(userReg.RegistrationRetryInterval);
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration failed with " + sipResponse.Status + " for " + userReg.Username + " on " + userReg.Registrar + ", DISABLING.", userReg.Owner));

                            //if (sipResponse.Status == SIPResponseStatusCodesEnum.NotFound)
                            //{
                            //    logger.Error("NotFound response for user " + userReg.Owner + "'s registration for " + userReg.Registrar + ".");
                            //    logger.Error(sipResponse.ToString());
                            //}

                            userReg.DeleteRequired = true;
                            userReg.Disabled = true;
                        }
                        else if (sipResponse.Status == SIPResponseStatusCodesEnum.IntervalTooBrief ||
                                 (sipResponse.Status == SIPResponseStatusCodesEnum.BusyEverywhere && sipResponse.ReasonPhrase == "Too Frequent Requests")) // FWD uses this to indicate it doesn't like the timing.
                        {
                            userReg.Registered = false;
                            userReg.FailureMessage = "Authentication failed (" + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + ").";
                            userReg.RegistrationRetryInterval = REGISTER_FAILURERETRY_INTERVAL + Crypto.GetRandomInt(0, REGISTER_FAILURERETRY_INTERVAL);
                            userReg.NextRegistrationTime = DateTime.Now.AddSeconds(userReg.RegistrationRetryInterval);
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration failed for " + userReg.Username + " on " + userReg.Registrar + " due to " + sipResponse.ReasonPhrase + " , retry interval extended to " + userReg.RegistrationRetryInterval + "s.", userReg.Owner));
                        }
                        else
                        {
                            userReg.Registered = false;
                            userReg.FailureMessage = "Authentication failed (" + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + ").";
                            userReg.NextRegistrationTime = DateTime.Now.AddSeconds(userReg.RegistrationRetryInterval);
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration failed with " + sipResponse.Status + " for " + userReg.Username + " on " + userReg.Registrar + ".", userReg.Owner));
                        }

                        RegistrationComplete(callIdGuid);
                    }
                }
                else
                {
                    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.Warn, "An " + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + " from " + remoteEndPoint + " was received for an unknown call.", null));
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPRegistrationAgent ServerResponseReceived (" + remoteEndPoint + "). " + excp.Message);
            }
        }

        /// <summary>
        /// The event handler for responses to the authenticated register request.
        /// </summary>
        private void AuthResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse)
        {
            try
            {
                Guid callIdGuid = new Guid(sipResponse.Header.CallId);

                if (m_registrationsById.ContainsKey(callIdGuid))
                {
                    UserRegistration userReg = m_registrationsById[callIdGuid];

                    if (userReg.ContactRegisterType == SIPContactRegistrationEnum.Removebinding)
                    {
                        // Check if this was a binding removal request and put out a log message if it is.
                        if (sipResponse.Status == SIPResponseStatusCodesEnum.Ok)
                        {
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRemoval, "Ok received on contact removal request for " + userReg.Username + " and " + userReg.Registrar + ".", userReg.Owner));
                            // Updated contacts list.
                            userReg.ContactsList = sipResponse.Header.Contact;
                        }
                        else
                        {
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "A contact removal request failed with " + sipResponse.Status + " for " + userReg.Username + " and " + userReg.Registrar + ".", userReg.Owner));
                        }
                    }
                    else if (userReg.ContactRegisterType == SIPContactRegistrationEnum.CheckContacts)
                    {
                        // Check if this was a binding referesh request and put out a log message if it is.
                        if (sipResponse.Status == SIPResponseStatusCodesEnum.Ok)
                        {
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegistered, "Ok received on contact refresh request for " + userReg.Username + " and " + userReg.Registrar + ".", userReg.Owner));
                            // Updated contacts list.
                            userReg.ContactsList = sipResponse.Header.Contact;
                        }
                        else
                        {
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "A contact refresh request failed with " + sipResponse.Status + " for " + userReg.Username + " and " + userReg.Registrar + ".", userReg.Owner));
                        }
                    }
                    else
                    {
                        // Normal register request.
                        if (sipResponse.Status == SIPResponseStatusCodesEnum.Ok)
                        {
                            OkResponseReceived(sipTransaction, remoteEndPoint, sipResponse);
                        }
                        else if (sipResponse.Status == SIPResponseStatusCodesEnum.IntervalTooBrief)
                        {
                            userReg.Registered = false;
                            userReg.FailureMessage = "Authentication failed (" + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + ").";
                            userReg.RegistrationRetryInterval += REGISTER_FAILURERETRY_INTERVAL;
                            userReg.NextRegistrationTime = DateTime.Now.AddSeconds(userReg.RegistrationRetryInterval);
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration failed for " + userReg.Username + " on " + userReg.Registrar + " due to " + sipResponse.ReasonPhrase + " , retry interval extended to " + userReg.RegistrationRetryInterval + "s.", userReg.Owner));
                        }
                        else if (sipResponse.Status == SIPResponseStatusCodesEnum.Forbidden || sipResponse.Status == SIPResponseStatusCodesEnum.NotFound)
                        {
                            // SIP account does not appear to exist. Disable registration attempts until user intervenes to correct.
                            userReg.Registered = false;
                            userReg.FailureMessage = "Authentication failed (" + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + ").";
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration failed with " + sipResponse.Status + " for " + userReg.Username + " on " + userReg.Registrar + ", DISABLING.", userReg.Owner));
                            userReg.DeleteRequired = true;
                            userReg.Disabled = true;
                        }
                        else
                        {
                            userReg.Registered = false;
                            userReg.FailureMessage = "Authentication failed (" + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + ").";

                            //logger.Warn("Registration failed for " + userReg.Username + " and " + userReg.Registrar + " owner " + userReg.Owner + ".");
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Registration failed for " + userReg.Username + " on " + userReg.Registrar + " with " + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + ".", userReg.Owner));
                        }
                    }

                    RegistrationComplete(callIdGuid);
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPRegistrationAgent AuthResponseReceived. " + excp.Message);
            }
        }

        private void OkResponseReceived(SIPTransaction sipTransaction, IPEndPoint remoteEndPoint, SIPResponse sipResponse)
        {
            try
            {
                Guid callIdGuid = new Guid(sipResponse.Header.CallId);

                if (m_registrationsById.ContainsKey(callIdGuid))
                {
                    UserRegistration userReg = m_registrationsById[callIdGuid];

                    // Updated contacts list.
                    userReg.ContactsList = sipResponse.Header.Contact;

                    userReg.Registered = true;
                    userReg.LastRegisterTime = DateTime.Now;
                    userReg.FailureMessage = null;

                    // Find the contact in the list that matches the one being maintained by this agent in order to determine the expiry value.
                    int headerExpires = sipResponse.Header.Expires;
                    int contactExpires = -1;
                    if (userReg.ContactsList != null && userReg.ContactsList.Count > 0)
                    {
                        foreach (SIPContactHeader contactHeader in userReg.ContactsList)
                        {
                            if (contactHeader.ContactURI.Parameters.Get(UserRegistration.CONTACT_ID_KEY) == userReg.Contact.Parameters.Get(UserRegistration.CONTACT_ID_KEY))
                            {
                                contactExpires = contactHeader.Expires;
                                break;
                            }
                        }
                    }

                    if (contactExpires != -1 && contactExpires >= UserRegistration.MINIMUM_ALLOWED_EXPIRY && contactExpires <= UserRegistration.MAXIMUM_ALLOWED_EXPIRY)
                    {
                        userReg.ExpirySeconds = contactExpires;
                    }
                    else if (headerExpires != -1 && headerExpires >= UserRegistration.MINIMUM_ALLOWED_EXPIRY && headerExpires <= UserRegistration.MAXIMUM_ALLOWED_EXPIRY)
                    {
                        userReg.ExpirySeconds = headerExpires;
                    }
                    else
                    {
                        userReg.ExpirySeconds = userReg.OriginalExpirySeconds;
                    }

                    userReg.NextRegistrationTime = DateTime.Now.AddSeconds(userReg.ExpirySeconds - REGISTRATION_HEAD_TIME);
                    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegistered, "Contact successfully registered for " + userReg.Username + " on " + userReg.Registrar + ", expiry " + userReg.ExpirySeconds + "s.", userReg.Owner));
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPRegistrationAgent OkResponseReceived. " + excp.Message);
            }
        }

		public SIPRequest GetRegistrationRequest(UserRegistration userRegistration, IPEndPoint localEndPoint, int expiry, IPEndPoint registrarIPAddress)
		{	
			try
			{
                string realm = (userRegistration.Realm != null && userRegistration.Realm.Trim().Length > 0) ? userRegistration.Realm : userRegistration.Registrar;

                SIPURI registerURI = SIPURI.ParseSIPURI("sip:" + realm);
                SIPURI regUserURI = SIPURI.ParseSIPURI("sip:" + userRegistration.Username + "@" + realm);

                SIPFromHeader fromHeader = new SIPFromHeader(null, regUserURI, CallProperties.CreateNewTag());
                SIPToHeader toHeader = new SIPToHeader(null, regUserURI, null);
				SIPContactHeader contactHeader = new SIPContactHeader(null, userRegistration.Contact);
                string callId = userRegistration.RegistrationId.ToString();
                int cseq = userRegistration.CSeq;	
				
                SIPRequest registerRequest = new SIPRequest(SIPMethodsEnum.REGISTER, registerURI);

				SIPHeader header = new SIPHeader(contactHeader, fromHeader, toHeader, cseq, callId);
				header.CSeqMethod = SIPMethodsEnum.REGISTER;
                header.UserAgent = SIP_USERAGENT_STRING;
                header.Expires = userRegistration.OriginalExpirySeconds;

                //string branchId = SIPConstants.SIP_BRANCH_MAGICCOOKIE + Crypto.GetRandomInt().ToString();
                SIPViaHeader viaHeader = new SIPViaHeader(localEndPoint.Address.ToString(), localEndPoint.Port, CallProperties.CreateBranchId());
				header.Via.PushViaHeader(viaHeader);

                SIPRoute registrarRoute = new SIPRoute("<sip:" + registrarIPAddress.ToString() + ";lr>");
                header.Routes.PushRoute(registrarRoute);

                if (userRegistration.CustomHeaders != null && userRegistration.CustomHeaders.Trim().Length > 0)
                {
                    string[] customerHeadersList = userRegistration.CustomHeaders.Split(CUSTOM_HEADERS_SEPARATOR);

                    if (customerHeadersList != null && customerHeadersList.Length > 0)
                    {
                        foreach (string customHeader in customerHeadersList)
                        {
                            if (customHeader.IndexOf(':') == -1)
                            {
                                logger.Debug("Skipping custom header due to missing colon, " + customHeader + ".");
                                continue;
                            }
                            else
                            {
                                string headerName = customHeader.Substring(0, customHeader.IndexOf(':'));
                                if (headerName != null && Regex.Match(headerName.Trim(), "(Via|From|To|Contact|CSeq|Call-ID|Max-Forwards|Content)", RegexOptions.IgnoreCase).Success)
                                {
                                    logger.Debug("Skipping custom header due to an non-permitted string in header name, " + customHeader + ".");
                                    continue;
                                }
                                else
                                {
                                    if (headerName == SIPConstants.SIP_USERAGENT_STRING)
                                    {
                                        header.UserAgent = customHeader.Substring(customHeader.IndexOf(':') + 1);
                                    }
                                    else
                                    {
                                        header.UnknownHeaders.Add(customHeader.Trim());
                                    }
                                }
                            }
                        }
                    }
                }

                registerRequest.Header = header;
                return registerRequest;
			}
			catch(Exception excp)
			{
                logger.Error("Exception GetRegistrationRequest. " + excp.Message);
				throw excp;
			}
		}

        public void SendContactRemovalRequest(Guid registrationId, SIPURI contactURI)
        {
            try
            {
                UserRegistration userReg = m_registrationsById[registrationId];

                if (userReg != null)
                {
                    userReg.DeleteSent = DateTime.Now;
                    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRemoval, "Submitting contact removal request for " + userReg.Username + " and " + contactURI.ToString() + ".", userReg.Username));

                    userReg.CSeq = userReg.CSeq + 1;
                    userReg.ContactRegisterType = SIPContactRegistrationEnum.Removebinding;

                    SIPRequest regRequest = GetRegistrationRequest(userReg, m_sipTransport.GetTransportContact(userReg.RegistrarIPEndPoint), 0, userReg.RegistrarIPEndPoint);
                    regRequest.Header.Expires = 0;
                    SIPContactHeader contactHeader = new SIPContactHeader(null, contactURI);
                    regRequest.Header.Contact = new List<SIPContactHeader>();
                    regRequest.Header.Contact.Add(contactHeader);
                    regRequest.Header.Contact[0].Expires = 0;

                    SIPNonInviteTransaction regTransaction = m_sipTransport.CreateNonInviteTransaction(regRequest, m_proxyEndPoint, m_sipTransport.GetTransportContact(null), SIPProtocolsEnum.UDP);
                    regTransaction.NonInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(ServerResponseReceived);

                    //m_sipTransport.SendRequest(userReg.RegistrarIPEndPoint, userReg.Protocol, regRequest);
                    m_sipTransport.SendSIPReliable(regTransaction);
                }
                else
                {
                    logger.Warn("Could not find user registration record for " + registrationId + " in SendContactRemovalRequest.");
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SendContactRemovalRequest. " + excp.Message);
            }
        }

        /// <summary>
        /// Sends a registration request with an empty contact list. The SIP Registrar should return a list of all the currently registered contacts allowing 
        /// the list held to be refreshed.
        /// </summary>
        /// <param name="registrationId"></param>
        public void SendEmptyRegistrationRequest(Guid registrationId)
        {
            try
            {
                UserRegistration userReg = m_registrationsById[registrationId];

                if (userReg != null)
                {
                    FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRefresh, "Submitting registration refresh request for " + userReg.Username + " and " + userReg.Registrar + ".", userReg.Username));

                    if (userReg.RegistrarIPEndPoint != null)
                    {
                        userReg.CSeq = userReg.CSeq + 1;
                        userReg.ContactRegisterType = SIPContactRegistrationEnum.CheckContacts;

                        SIPRequest regRequest = GetRegistrationRequest(userReg, m_sipTransport.GetTransportContact(userReg.RegistrarIPEndPoint), 0, userReg.RegistrarIPEndPoint);
                        regRequest.Header.Expires = -1;                             // Stops the Expires header being sent.
                        regRequest.Header.Contact = new List<SIPContactHeader>();   // Stops the Contact header being sent.

                        //SIPNonInviteTransaction regTransaction = m_sipTransport.CreateNonInviteTransaction(regRequest, userReg.RegistrarIPEndPoint, m_sipTransport.GetTransportContact(null), SIPProtocolsEnum.UDP);
                        SIPNonInviteTransaction regTransaction = m_sipTransport.CreateNonInviteTransaction(regRequest, m_proxyEndPoint, m_sipTransport.GetTransportContact(null), SIPProtocolsEnum.UDP);
                        regTransaction.NonInviteTransactionFinalResponseReceived += new SIPTransactionResponseReceivedDelegate(ServerResponseReceived);

                        //m_sipTransport.SendRequest(userReg.RegistrarIPEndPoint, userReg.Protocol, regRequest);
                        m_sipTransport.SendSIPReliable(regTransaction);
                    }
                    else
                    {
                        FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRegisterFailed, "Could not resolve server and submit a refresh request for " + userReg.Registrar + ".", userReg.Username));
                    }
                }
                else
                {
                    logger.Warn("Could not find user registration record for " + registrationId + " in SendEmptyRegistrationRequest.");
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SendEmptyRegistrationRequest. " + excp.Message);
            }
        }

        private void ExpireAllCurrentRegistrations()
        {
            try
            {
                logger.Debug("Commencing ExpireAllCurrentRegistrations.");

                foreach (UserRegistration userReg in m_registrationsById.Values)
                {
                    if (userReg.Registered)
                    {
                        SendContactRemovalRequest(userReg.RegistrationId, userReg.Contact);
                    }
                }

                // Give the registration expiry requests 
                Thread.Sleep(REGISTER_EXPIREALL_WAITTIME);

                logger.Debug("ExpireAllCurrentRegistrations complete.");
            }
            catch (Exception excp)
            {
                logger.Error("Exception ExpireAllCurrentRegistrations. " + excp.Message);
            }
        }

        /// <summary>
        /// After a registration is complete or times out the callid needs to be removed from the state lists.
        /// </summary>
        /// <param name="callId"></param>
        private void RegistrationComplete(Guid callId)
        {
            try
            {
                if (m_registrationsById.ContainsKey(callId))
                {
                    UserRegistration userReg = m_registrationsById[callId];

                    if (m_registrationsById[callId].DeleteRequired)
                    {
                        lock (m_registrationsById)
                        {
                            FireProxyLogEvent(new ProxyMonitorEvent(ProxyServerTypesEnum.RegisterAgent, ProxyEventTypesEnum.ContactRemoval, "Registration deletion complete for " + m_registrationsById[callId].Contact.ToString() + ".", m_registrationsById[callId].Owner));
                            m_registrationsById.Remove(callId);
                        }
                    }
                    else
                    {
                        m_registrationsById[callId].RegisterInProgress = false;
                    }

                    if (RegistrationStateChanged != null)
                    {
                        RegistrationStateChanged(userReg);
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception RemoveCallIdState. " + excp.Message);
            }
        }

        public void Stop()
        {
            try
            {
                m_sendRegisters = false;
                ExpireAllCurrentRegistrations();
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPRegistrationAgent Stop. " + excp.Message);
            }
        }

        private void FireProxyLogEvent(ProxyMonitorEvent monitorEvent)
        {
            if (StatefulProxyLogEvent != null)
            {
                try
                {
                    StatefulProxyLogEvent(monitorEvent);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception FireProxyLogEvent SIPRegistrationAgent. " + excp.Message);
                }
            }
        }
	}
}
